跳到主要内容

Spring 事务的使用~

如何使用事务

参考资料 Spring+Mybatis配置事务管理

声明式事务就是指在配置文件中声明,用在 Spring 配置文件中声明的方式来代替编程式事务,声明管理不会侵入开发的组件,业务逻辑无需再去处理事务,而把事务交给系统层面去管理。而 Spring 声明式事务控制的底层就是 AOP

注意:为事务管理器指定的 DataSource 必须和用来创建 SqlSessionFactoryBean 的是同一个数据源,否则事务管理器就无法工作了。

添加依赖

这里使用的是 JDBC 来做持久层框架

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>

然后导入 Spring 的事务管理依赖

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>

配置 TransactionManager

要开启 Spring 的事务处理功能,在 Spring 的配置文件中创建一个 DataSourceTransactionManager 对象:

这个 PlatformTransactionManager 接口对象传入的类在不同平台的事务管理器都不同

<!-- 这个就是上面继承了 Spring 的 PlatformTransactionManager 接口的对象(MyBatis 和 jdbc 的都叫这个) -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="dataSource" />
</bean>

如果是使用注解配置

@Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}

把上面的 DataSource 交由 TransactionManager 管理后再进行下列配置

配置 XML

添加上两个标签:txaop(分别引入事务管理和 AOP 的命名空间) 注意:这个引入的标签是 tx 结尾的

修改 xml 头

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

创建目标对象

目标对象,里面就一个 transfer(转账方法)

public class AccountServiceImpl implements AccountService {

private AccountDao accountDao;
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}

public void transfer(String outMan, String inMan, double money) {
// 这个转账方法由 out 和 in 组成,配置事务使之只要一个错了就会回滚
accountDao.out(outMan,money);
accountDao.in(inMan,money);
}
}

声明事务

之前写 AOP 的时候还需要自己写增强方法,但是到了这个事务管理可以使用 Spring 提供的事务管理命名空间 tx

在 Spring 里配置这个事务,如下所示:

<!-- 目标对象,内部的方法就是切点 -->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"/>
</bean>

<!-- 把 DataSource 交由 TransactionManager 管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="dataSource"/>
</bean>


<!-- 配置:通知(事务的增强) -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- name 就是字符匹配,这里通过字符匹配来找到需要切入的某种业务方法(所以需要命名规范)-->
<tx:attributes>
<!--加上这个之后对所有的进行事务管理,rollback-for是捕捉到异常就回滚-->
<tx:method name="*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
</tx:attributes>
</tx:advice>


<!-- 配置事务 AOP 织入 -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut id="txPointCut" expression="execution(* com.alsritter.mapper.*.*(..))"/>

<!-- 注入通知
注意:这里使用的是 advisor 所以织入的通知要继承那几个通知接口
而前面配置的那个 “事务的增强” 就是继承了这个接口的通知,所以可以直接织入进来 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>

使用 XML 设置事务的属性

上面在学习 TransactionDefinition 接口时已经说明了事务的隔离级别和传播行为,这里详细说明如何配置

下面的这个 <tx:attributes> 就是用来设置 事务 的属性的

其中可以设置的属性为

<tx:method name="query*" isolation="DEFAULT" propagation="SUPPORTS"  timeout="-1" read-only="true" />

它的各个属性的作用:

  • isolation:设置隔离级别
  • propagation:设置传播特性
  • timeout:设置超时时间
<!-- 把 DataSource 交由 TransactionManager 管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="dataSource"/>
</bean>

<!-- 事务相关控制配置:例如配置事务的传播机制 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">

<!-- 这个 <tx:attributes> 就是用来设置 事务 的属性的 -->
<!-- name 用来匹配方法(所以需要命名规范)-->
<tx:attributes>
<!-- 例如上面那个叫做 transfer 的方法可以设置一下 -->
<tx:method name="transfer" propagation="REQUIRED"/>
<!-- 同理,下面就是对一些方法名进行匹配 -->
<tx:method name="get*" propagation="SUPPORTS" read-only="true" />
<tx:method name="find*" propagation="SUPPORTS" read-only="true" />
<tx:method name="load*" propagation="SUPPORTS" read-only="true" />
<tx:method name="read*" propagation="SUPPORTS" read-only="true" />
<tx:method name="query*" propagation="SUPPORTS" read-only="true" />
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>

<!--加上这个之后对所有的进行事务管理,rollback-for是捕捉到异常就回滚-->
<tx:method name="*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
</tx:attributes>
</tx:advice>

<!--配置事务切入-->
<aop:config>
<!--配置切入点-->
<aop:pointcut id="txPointCut" expression="execution(* com.alsritter.mapper.*.*(..))"/>
<!--注入通知-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>

注解的方式配置

执行使用 @Transactional 注解就行了

@Transactional 的作用范围

  1. 方法:推荐将注解使用于方法上,不过需要注意的是,该注解只能应用到 public 方法上,否则不生效。
  2. 类:如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
  3. 接口:不推荐在接口上使用。

常用的一些属性配置

属性名说明
name当在配置文件中有多个 TransactionManager , 可以用该属性指定选择哪个事务管理器。
propagation事务的传播行为,默认值为 REQUIRED。
isolation事务的隔离度,默认值采用 DEFAULT。
timeout事务的超时时间,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
read-only指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。
rollback-for用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。
no-rollback- for抛出 no-rollback-for 指定的异常类型,不回滚事务。

使用方式如下所示:

@Transactional(propagation= Propagation.SUPPORTS, readOnly=true)
@Service(value ="employeeService")
public class EmployeeService

注解的使用示例

主要的注解就是 @Transactional

1、先在 Spring 配置文件里启动注解

<!--启动组件扫描-->
<context:component-scan base-package="com.itheima"/>

<!--事物的注解驱动-->
<tx:annotation-driven transaction-manager="transactionManager"/>

注意:在 SpringBoot 之后直接使用就行了,默认打开了~

2、编写 Dao 层

// 这个 @Repository 是 dao 层的 Component 注解
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {

@Autowired
private JdbcTemplate jdbcTemplate;

public void out(String outMan, double money) {
jdbcTemplate.update("update account set money=money-? where name=?",money,outMan);
}

public void in(String inMan, double money) {
jdbcTemplate.update("update account set money=money+? where name=?",money,inMan);
}
}

3、把 Service 层加上事务管理

@Service("accountService")
// 配置事务的信息(如果方法上也有 @Transactional 则使用方法上的)
@Transactional(isolation = Isolation.REPEATABLE_READ)
public class AccountServiceImpl implements AccountService {

@Autowired
private AccountDao accountDao;

@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public void transfer(String outMan, String inMan, double money) {
accountDao.out(outMan,money);
int i = 1/0;
accountDao.in(inMan,money);
}

// 可以单独给不同的方法定义事务属性(同那个配置文件一样)
@Transactional(isolation = Isolation.DEFAULT)
public void xxx(){}
}

事务注解原理

我们知道,@Transactional 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理(所以让每个 Service 都先编写一个接口也有一部分这个原因,因为 JDK Proxy 的效率大于 CGLIB)。

多提一嘴:createAopProxy() 方法 决定了是使用 JDK 还是 Cglib 来做动态代理,源码如下:

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
.......
}

如果一个类或者一个类中的 public 方法上被标注 @Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被 @Transactional 注解的 public 方法的时候,实际调用的是,TransactionInterceptor 类中的 invoke() 方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。

注意:TransactionInterceptor 类中的 invoke() 方法内部实际调用的是 TransactionAspectSupport 类的 invokeWithinTransaction() 方法。由于新版本的 Spring 对这部分重写很大,而且用到了很多响应式编程的知识,这里就不列源码了。

Spring AOP 自调用问题

若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有 @Transactional 注解的方法的事务会失效。

这是由于 Spring AOP 代理的原因造成的,因为只有当 @Transactional 注解的方法在类以外被调用的时候,Spring 事务管理才生效。

注意:在同一个类中一个无事务的方法调用另一个有事务的方法,虽然事务是不会起作用的,但是一般 IDEA 会提示,所以也无需太担心忘记了

如下所示:MyService 类中的 method1() 调用 method2() 就会导致 method2() 的事务失效。

@Service
public class MyService {

private void method1() {
method2();
//......
}

@Transactional
public void method2() {
//......
}
}

解决办法就是避免同一类中自调用或者使用 AspectJ 取代 Spring AOP 代理。

rollbackFor 捕获自定义异常

参考资料 @Transactional(rollbackFor = Exception.class)注解了解吗?

默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚。 但是你可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚。

同样,你还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。

注意:RuntimeException 运行时异常和 Exception 非运行时异常。

事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。

@Transactional 默认只捕获 RuntimeException,而自定义异常捕获后也不会回滚

注意:当 @Transactional 注解 作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常(RuntimeException),就会回滚,数据库里面的数据也会回滚。

但是如果在 @Transactional 注解中如果不配置 rollbackFor 属性(即默认的情况),那么事务只会在遇到 RuntimeException 的时候才会回滚,加上 rollbackFor = Exception.class,可以让事务在遇到非运行时异常时也回滚。

使用例:

@Override
@Transactional(rollbackFor = TestException.class)
public void transOuter() {
productMapper.updateOrderQuantityPessimistic(product_code1);
((ProductService) AopContext.currentProxy()).transInner();
}

@Transactional(rollbackFor = Exception.class)
public void transInner() {
productMapper.updateOrderQuantityPessimistic(product_code);
if (true) {
throw new RuntimeException();
}
}

这样,自己抛出的 TestException 错误也能回滚~